package core

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"github.com/ethereum/go-ethereum/rlp"
	"strings"
)

// A Transaction is a full transaction object containing a payload and signatures.
type Transaction struct {
	// Script is the UTF-8 encoded Cadence source code that defines the execution logic for this transaction.
	Script []byte

	// Arguments is a list of Cadence values passed into this transaction.
	//
	// Each argument is encoded as JSON-CDC bytes.
	Arguments [][]byte

	// ReferenceBlockID is a reference to the block used to calculate the expiry of this transaction.
	//
	// A transaction is considered expired if it is submitted to Flow after refBlock + N, where N
	// is a constant defined by the network.
	//
	// For example, if a transaction references a block with height of X and the network limit is 10,
	// a block with height X+10 is the last block that is allowed to include this transaction.
	ReferenceBlockID Identifier

	// GasLimit is the maximum number of computational units that can be used to execute this transaction.
	GasLimit uint64

	// ProposalKey is the account key used to propose this transaction.
	//
	// A proposal key references a specific key on an account, along with an up-to-date
	// sequence number for that key. This sequence number is used to prevent replay attacks.
	//
	// You can find more information about sequence numbers here: https://docs.onflow.org/concepts/transaction-signing/#sequence-numbers
	ProposalKey ProposalKey

	// Payer is the account that pays the fee for this transaction.
	//
	// You can find more information about the payer role here: https://docs.onflow.org/concepts/transaction-signing/#signer-roles
	Payer Address

	// Authorizers is a list of the accounts that are authorizing this transaction to
	// mutate to their on-chain account state.
	//
	// You can find more information about the authorizer role here: https://docs.onflow.org/concepts/transaction-signing/#signer-roles
	Authorizers []Address

	// PayloadSignatures is a list of signatures generated by the proposer and authorizer roles.
	//
	// A payload signature is generated over the inner portion of the transaction (TransactionDomainTag + payload).
	//
	// You can find more information about transaction signatures here: https://docs.onflow.org/concepts/transaction-signing/#anatomy-of-a-transaction
	PayloadSignatures []TransactionSignature

	// EnvelopeSignatures is a list of signatures generated by the payer role.
	//
	// An envelope signature is generated over the outer portion of the transaction (TransactionDomainTag + payload + payloadSignatures).
	//
	// You can find more information about transaction signatures here: https://docs.onflow.org/concepts/transaction-signing/#anatomy-of-a-transaction
	EnvelopeSignatures []TransactionSignature
}

type payloadCanonicalForm struct {
	Script                    []byte
	Arguments                 [][]byte
	ReferenceBlockID          []byte
	GasLimit                  uint64
	ProposalKeyAddress        []byte
	ProposalKeyIndex          uint64
	ProposalKeySequenceNumber uint64
	Payer                     []byte
	Authorizers               [][]byte
}

type envelopeCanonicalForm struct {
	Payload           payloadCanonicalForm
	PayloadSignatures []transactionSignatureCanonicalForm
}

type transactionSignatureCanonicalForm struct {
	SignerIndex uint
	KeyIndex    uint
	Signature   []byte
}

type signaturesList []TransactionSignature

func (s signaturesList) canonicalForm() []transactionSignatureCanonicalForm {
	signatures := make([]transactionSignatureCanonicalForm, len(s))

	for i, signature := range s {
		signatures[i] = signature.canonicalForm()
	}

	return signatures
}

func (s TransactionSignature) canonicalForm() transactionSignatureCanonicalForm {
	return transactionSignatureCanonicalForm{
		SignerIndex: uint(s.SignerIndex), // int is not RLP-serializable
		KeyIndex:    uint(s.KeyIndex),    // int is not RLP-serializable
		Signature:   s.Signature,
	}
}

// An Identifier is a 32-byte unique identifier for an entity.
type Identifier [32]byte

// A ProposalKey is the key that specifies the proposal key and sequence number for a transaction.
type ProposalKey struct {
	Address        Address
	KeyIndex       int
	SequenceNumber uint64
}

const AddressLength = 8

// Address represents the 8 byte address of an account.
type Address [AddressLength]byte

func (addr Address) String() string {
	return hex.EncodeToString(addr[:])
}

func (addr Address) Bytes() []byte {
	return addr[:]
}

// A TransactionSignature is a signature associated with a specific account key.
type TransactionSignature struct {
	Address     Address
	SignerIndex int
	KeyIndex    int
	Signature   []byte
}

// HexToAddress converts a hex string to an Address.
func HexToAddress(h string) Address {
	trimmed := strings.TrimPrefix(h, "0x")
	if len(trimmed)%2 == 1 {
		trimmed = "0" + trimmed
	}
	b, _ := hex.DecodeString(trimmed)
	return BytesToAddress(b)
}

// BytesToAddress returns Address with value b.
//
// If b is larger than 8, b will be cropped from the left.
// If b is smaller than 8, b will be appended by zeroes at the front.
func BytesToAddress(b []byte) Address {
	var a Address
	if len(b) > AddressLength {
		b = b[len(b)-AddressLength:]
	}
	copy(a[AddressLength-len(b):], b)
	return a
}

// BytesToID constructs an identifier from a byte slice.
func BytesToID(b []byte) Identifier {
	var id Identifier
	copy(id[:], b)
	return id
}

// HexToID constructs an identifier from a hexadecimal string.
func HexToID(h string) Identifier {
	b, _ := hex.DecodeString(h)
	return BytesToID(b)
}

const ContractsParam = `{"type":"Dictionary","value":[]}`

type FlowPublicKey [64]byte

const PublicKeyParamTpl = `{"type":"Array","value":[{"type":"Struct","value":{"id":"I.Crypto.Crypto.KeyListEntry","fields":[{"name":"keyIndex","value":{"type":"Int","value":"1000"}},{"name":"publicKey","value":{"type":"Struct","value":{"id":"PublicKey","fields":[{"name":"publicKey","value":{"type":"Array","value":%s}},{"name":"signatureAlgorithm","value":{"type":"Enum","value":{"id":"SignatureAlgorithm","fields":[{"name":"rawValue","value":{"type":"UInt8","value":"1"}}]}}}]}}},{"name":"hashAlgorithm","value":{"type":"Enum","value":{"id":"HashAlgorithm","fields":[{"name":"rawValue","value":{"type":"UInt8","value":"3"}}]}}},{"name":"weight","value":{"type":"UFix64","value":"1000.00000000"}},{"name":"isRevoked","value":{"type":"Bool","value":false}}]}}]}`

func (pk FlowPublicKey) FromBytes(bytes []byte) FlowPublicKey {
	if len(bytes) != 64 {
		return pk
	}

	for i, b := range bytes {
		pk[i] = b
	}

	return pk
}

func (pk FlowPublicKey) ToParam() string {
	type value struct {
		T string `json:"type,omitempty"`
		V string `json:"value,omitempty"`
	}

	values := make([]value, len(pk))

	for i, b := range pk {
		v := value{
			T: "UInt8",
			V: fmt.Sprintf("%d", b),
		}

		values[i] = v
	}

	bytes, _ := json.Marshal(values)

	return fmt.Sprintf(PublicKeyParamTpl, string(bytes))

}

func (t *Transaction) PayloadMessage() []byte {
	temp := t.payloadCanonicalForm()
	return mustRLPEncode(&temp)
}

func (t *Transaction) payloadCanonicalForm() payloadCanonicalForm {
	authorizers := make([][]byte, len(t.Authorizers))
	for i, auth := range t.Authorizers {
		authorizers[i] = auth.Bytes()
	}

	return payloadCanonicalForm{
		Script:                    t.Script,
		Arguments:                 t.Arguments,
		ReferenceBlockID:          t.ReferenceBlockID[:],
		GasLimit:                  t.GasLimit,
		ProposalKeyAddress:        t.ProposalKey.Address.Bytes(),
		ProposalKeyIndex:          uint64(t.ProposalKey.KeyIndex),
		ProposalKeySequenceNumber: t.ProposalKey.SequenceNumber,
		Payer:                     t.Payer.Bytes(),
		Authorizers:               authorizers,
	}
}

// EnvelopeMessage returns the signable message for the transaction envelope.
//
// This message is only signed by the payer account.
func (t *Transaction) EnvelopeMessage() []byte {
	temp := t.envelopeCanonicalForm()
	return mustRLPEncode(&temp)
}

func (t *Transaction) envelopeCanonicalForm() envelopeCanonicalForm {
	return envelopeCanonicalForm{
		Payload:           t.payloadCanonicalForm(),
		PayloadSignatures: signaturesList(t.PayloadSignatures).canonicalForm(),
	}
}

func rlpEncode(v interface{}) ([]byte, error) {
	return rlp.EncodeToBytes(v)
}

func mustRLPEncode(v interface{}) []byte {
	b, err := rlpEncode(v)
	if err != nil {
		panic(err)
	}
	return b
}
